#include "Application.h"
#include "Core/Vector3.h"
#include "Core/Vector2.h"
#include <exception>
#include <stdexcept>
#include <cassert>
#include <algorithm>
#include <iostream>
#define _USE_MATH_DEFINES
#include <cmath>
#include "glm/glm.h"
#include "Core/GlPlatform.h"

using namespace Core;
using namespace View;

void drawSpaceScene();

// Constructors
Application::Application() 
	: currentTicks( 0 )
	, previousTicks( 0 )
	, screenSize( 800, 600 )
	, pitching( 0 )
	, yawing( 0 )
	, rolling( 0 )
	, pitch( 0 )
	, yaw( M_PI )
	, roll( 0 )
	, mouseLook( false )
	, currentScene( 0 )
	, aspectMod( 1 )
	, viewMod( 1 )
	, mouseMoved(false)
{
	std::cout << 
		"Group Assignment #5 -- Team C\n"
		"\n"
		"Escape     Exit program\n"
		"\n"
		"W S        Move forward and backward, respectively\n"
		"A D        Strafe left and right, respectively\n"
		"Space,X    Move up and down, respectively\n"
		"\n"
		"I K        Look up and down, respectively\n"
		"J L        Look left and right, respectively\n"
		"U O        Roll left and right, respectively\n"
		"\n"
		"mbutton1   Click and drag to look with the mouse.\n"
		"\n"
		"1          Switch to city scene\n"
		"2          Switch to space scene\n"
		"\n"
		"[ ]        Expand and contract (respectively) the viewing angle\n"
		"; '        Expand and contract (respectively) the aspect ratio\n"
		"\n";

	std::fill( keys, keys + sizeof(keys)/sizeof(keys[0]), false );
	cam.mPosition = Point3f(-63.0f, 25.0f, 28.0f);
	cam.mOrientation = Quaternion4f::fromEuler(pitch, yaw, roll);
	cam.update();

	glutInitWindowSize(screenSize.x, screenSize.y);
	glutInitWindowPosition( -1, -1 );
	
	glutCreateWindow( "Team C - Group Assignment #5" );
	instanceMap.insert( std::pair<int,Application* const>( glutGetWindow(), this ) );

	glutReshapeFunc( reshape );
	glutKeyboardFunc( keydown );
	glutKeyboardUpFunc( keyup );
	glutMouseFunc( mouse );
	glutPassiveMotionFunc(motion);
	glutDisplayFunc( render );
	glutMotionFunc( motion );
	glutIdleFunc( idle );

	glDepthFunc(GL_LEQUAL);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_NORMALIZE);
	glEnable(GL_COLOR_MATERIAL);

	// Load model
	GLMmodel* m = glmReadOBJ("../media/city.obj");
	if ( !m )
		throw std::runtime_error( "Unable to load scene" );
	pmodel1.reset( m, glmDelete );

	cityScene = glGenLists( 1 );
	glNewList(cityScene, GL_COMPILE);
	GLfloat mat_specular[] = {1.0, 1.0, 1.0, 1.0};
	GLfloat mat_diffuse[] = {1.0, 1.0, 1.0, 1.0};
	GLfloat mat_ambient[] = {1.0, 1.0, 1.0, 0.2};
	GLfloat mat_shininess = {0};
	GLfloat light_ambient[] = {.5, .5, .5, 0.3};
	GLfloat light_diffuse[] = {1, 1, 1, 0.75};
	GLfloat light_specular[] = {0, 0, 0, 100};
	GLfloat light_position[] = {10.0, 20.0, 15.0, 0.0};
	
	glLightfv(GL_LIGHT0, GL_POSITION, light_position);
	glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
	glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);

	glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
	glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
	glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
	glMaterialf(GL_FRONT, GL_SHININESS, mat_shininess);

	glShadeModel(GL_SMOOTH);
	glEnable(GL_LIGHTING); 
	glEnable(GL_LIGHT0);
	glColor3ub( 96, 96, 255 );
	glmDraw(pmodel1.get(), GLM_SMOOTH); 
	glEndList();

	spaceScene = glGenLists( 1 );
	glNewList( spaceScene, GL_COMPILE );
	drawSpaceScene();
	glEndList();

	// This will rescale the object to fit into the unity matrix
	// Depending on your project you might want to keep the original size and positions you had in 3DS Max or GMAX so you may have to comment this.
	//glmUnitize(pmodel1);

	// Calculate normals. Not needed as this model has it built in.
	//glmFacetNormals(pmodel1);
	//glmVertexNormals(pmodel1, 90.0);
}


Application::~Application() {
	glDeleteLists(cityScene,1);
	int id = glutGetWindow();
	size_t ct = instanceMap.erase(id);
	assert( ct == 1 );
	glutDestroyWindow(id);
}

// Members
void Application::run() {
}

// Callbacks
void Application::onRender() {
	glClearColor( .2, .2, .2, 0 );
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	cam.update();
	cam.setup();

	glEnable( GL_CULL_FACE );
	glCullFace( GL_BACK );
	glDisable( GL_LIGHTING );
	/* 
	glBegin( GL_LINES ); {
		static const int c = 20;
		glColor3ub( 255, 255, 255 );
		for ( int z = -c; z <= c; ++z ) {
			for ( int x = -c; x <= c; ++x ) {
				glVertex3f(  x, 0, -c );
				glVertex3f(  x, 0,  c );
				glVertex3f( -c, 0,  z );
				glVertex3f(  c, 0,  z );
			}
		}

		static const unsigned u = 255;
		glColor3ub( u, 0, 0 ); glVertex3f(  0,  0,  0 ); glVertex3f( c,  0,  0 );
		glColor3ub( 0, u, 0 ); glVertex3f(  0,  0,  0 ); glVertex3f( 0,  c,  0 );
		glColor3ub( 0, 0, u ); glVertex3f(  0,  0,  0 ); glVertex3f( 0,  0,  c );
	} glEnd();
	*/ 

	glEnable( GL_LIGHTING );
	glCallList( (currentScene)?spaceScene:cityScene );
	// glColor3ub( 231, 160, 90 );
	// glmDraw(pmodel1.get(), GLM_SMOOTH); 

	glutSwapBuffers();
}

void Application::onKeyUp( unsigned char ch, int x, int y ) {
	keys[ch] = false;
}

void Application::onKeyDown( unsigned char ch, int x, int y ) {
	switch ( ch ) {
	case '1': currentScene = 0; break;
	case '2': currentScene = 1; break;
	default:;
	}
	keys[(int)ch] = true;
}

void Application::onMotion( int x, int y ) {
	using namespace Core;
	if ( mouseLook ) {
		Point2i pt = Point2i(x, y);
		Vector2i d = mouseAnchor - pt;
		mouseAnchor = Point2i(0,0) + screenSize.fromOrigin()/2;

		const float factor = M_PI/180;
		pitch += factor * d.y;
		yaw += factor * d.x;
		
		mouseMoved = true;
	}
}

void Application::onMouse( int button, int state, int x, int y ) {
	if ( button == 1 ) {
		mouseLook = state == GLUT_DOWN;
		mouseAnchor = Point2i( x, y );
	}
}

void Application::onMenu( int idx ) {
}

void Application::onReshape( int w, int h ) {
	glViewport(0, 0, w, h );
	
	double aspect = (double)w/h;
	double a = std::min( aspect, 1. );
	double b = std::max( aspect, 1. );
	
	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	gluPerspective(60.0 * viewMod, aspect * aspectMod, 1, 10000.0);
	screenSize = Core::Point2i( w, h );
	
	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
	// glRotatef( 60, 1, 0, 0 );
	// glTranslatef( 0, -5, 0 );
	// glRotatef( 45, 1, 0, 0 );
	// gluLookAt( 0, 0, 5, 0, 0, 0, 0, 0, 1 );
}

void Application::onIdle() {
	// Update
	previousTicks = currentTicks;
	currentTicks = glutGet( GLUT_ELAPSED_TIME );
	int elapsedTicks = currentTicks - previousTicks;
	float elapsed = elapsedTicks / 1000.f;

#if defined(WIN32)
	if ( keys[27] /* ESC */ )	PostQuitMessage(0);
#endif

	// Navigation
	float driving = 0.0;
	float strafing = 0.0;
	float floating = 0.0;
	if ( keys['w'] ) driving--;
	if ( keys['s'] ) driving++;
	if ( keys['d'] ) strafing++;
	if ( keys['a'] ) strafing--;
	if ( keys[' '] ) floating++;
	if ( keys['x'] ) floating--;
	
	float da = .2f * M_PI/180 * 360;
	if ( keys['i'] ) pitching+=da;
	if ( keys['k'] ) pitching-=da;
	if ( keys['j'] ) yawing+=da;
	if ( keys['l'] ) yawing-=da;
	if ( keys['u'] ) rolling+=da;
	if ( keys['o'] ) rolling-=da;

	// Viewport
	float aspect = 0;
	float viewing = 0;
	if ( keys['['] ) aspect--;
	if ( keys[']'] ) aspect++;
	if ( keys[';'] ) viewing--;
	if ( keys['\''] ) viewing++;
	
	float dmove = elapsed * 10 * 16;
	if ( driving || floating || strafing ) {
		cam.mPosition +=
			cam.forward() * (driving * dmove) +
			cam.right() * (strafing * dmove) +
			cam.up() * (floating * dmove);
	}

	if ( pitching || yawing || rolling ) {
		pitch += pitching * elapsed;
		yaw += yawing * elapsed;
		roll += rolling * elapsed;
		mouseMoved = true;
	}

	if ( mouseMoved ) {
		mouseMoved = false;
		cam.mOrientation = Quaternion4f::fromEuler( pitch, yaw, roll );
		pitching = yawing = rolling = 0;
		glutWarpPointer( screenSize.x/2, screenSize.y/2 );
	}

	float stretch = 1.f / 3 * elapsed;
	if ( viewing || aspect ) {
		aspectMod += stretch * aspect;
		viewMod += stretch * viewing;
		onReshape( screenSize.x, screenSize.y ); // Derp
	}

	cam.update();

	glutPostRedisplay();
	Sleep( 0 );
}


void Application::onSpecialKey( int key, int x, int y ) {
	
}


// Static glut callback handlers, mapped to instance members
void Application::reshape( int w, int h ) {
	instanceMap[glutGetWindow()]->onReshape( w, h );
}

void Application::keydown( unsigned char ch, int x, int y ) {
	instanceMap[glutGetWindow()]->onKeyDown( ch, x, y );
}

void Application::keyup( unsigned char ch, int x, int y ) {
	instanceMap[glutGetWindow()]->onKeyUp( ch, x, y );
}

void Application::mouse( int button, int state, int x, int y ) {
	instanceMap[glutGetWindow()]->onMouse( button, state, x, y );
}

void Application::render() {
	instanceMap[glutGetWindow()]->onRender();
}

void Application::motion( int x, int y ) {
	instanceMap[glutGetWindow()]->onMotion( x, y );
}

void Application::menu( int idx ) {
	instanceMap[glutGetWindow()]->onMenu( idx );
}

void Application::idle() {
	instanceMap[glutGetWindow()]->onIdle();
}

void Application::specialkey( int k, int x, int y ) {
	instanceMap[glutGetWindow()]->onSpecialKey( k, x, y );
}

// Helper members
std::map<int, Application* const> Application::instanceMap;
